/**
 * Copyright 2011  SURFfoundation
 * 
 * This file is part of ESCAPE.
 * 
 * ESCAPE is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * ESCAPE is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with ESCAPE.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * For more information:
 * http://escapesurf.wordpress.com/
 * http://www.surffoundation.nl/
 * 
 * Contact: d.vierkant@utwente.nl
 * 
 * @license http://www.gnu.org/licenses/gpl.html GNU GPLv3
 */

/*global window, document, Class, Ajax, $, $F, Element, Hash, Tooltip, alert, oreObjectEditorAggregationUri, predicateTableRowElementIdMappings */

// list of open calendars
var dhtmlxCalendarObjects = [];

// create a namespace
var escape_escapeUi = {};

escape_escapeUi.escapeHTML = function (text) {
    return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/'/g, '&#39;').replace(/"/g, '&quot;');
};

escape_escapeUi.gadgetMode = false;

/**
 * Get all URL hash parameters as an object.
 */
escape_escapeUi.getUrlHashParameters = function ()
{
	var parameters = {};
	
	if (window.location.hash !== null && window.location.hash !== '')
	{
		var hashText = window.location.hash.substring(1);

		parameters = hashText.parseQuery();
	}
	
	return parameters;
};

/**
 * Get a URL hash parameter by name.
 */
escape_escapeUi.getUrlHashParameter = function (name)
{
	return escape_escapeUi.getUrlHashParameters()[name];
};

/**
 * Set all URL hash parameters using an object.
 */
escape_escapeUi.setUrlHashParameters = function (parameters, replace) {
	if (replace)
	{
		var currentLocation = '' + window.location;
		currentLocation = currentLocation.replace(/#.*/, '');
		window.location.replace(currentLocation + '#' + Object.toQueryString(parameters));
	}
	else
	{
		window.location.hash = '#' + Object.toQueryString(parameters);
	}
};

/**
 * Set a URL hash parameter by name.
 */
escape_escapeUi.setUrlHashParameter = function (name, value, replace) {
	var parameters = escape_escapeUi.getUrlHashParameters();
	if (value === null)
	{
		delete parameters[name];
	}
	else
	{
		parameters[name] = value;
	}
	escape_escapeUi.setUrlHashParameters(parameters, replace);
};

/**
 * Poll a URL hash parameter on an interval and call an event handler if the parameter value changes (passing the new value as the first parameter).
 * 
 * Usage:
 * var urlHashParameterPoller = new escape_escapeUi.UrlHashParameterPoller('parameter_name', myTimeoutInMilliSeconds, myFunction);
 */
escape_escapeUi.UrlHashParameterPoller = Class.create({
	parameterName: null,
	intervalId: null,
	eventHandler: null,
	
	previousValue: null,
	
	initialize: function (parameterName, interval, eventHandler)
	{
		this.parameterName = parameterName;
		this.eventHandler = eventHandler;

		this.previousValue = escape_escapeUi.getUrlHashParameter(this.parameterName); 
		
		this.intervalId = setInterval(this.onInterval.bind(this), interval);
	},
	
	stop: function ()
	{
		clearInterval(this.intervalId);
	},
	
	onInterval: function ()
	{
		var value = escape_escapeUi.getUrlHashParameter(this.parameterName);
		if (value !== this.previousValue)
		{
			this.previousValue = value;
			this.eventHandler(value);
		}
	}
});

/**
 * Dynamically update the page content based on events from the graph viewer or changes in the URL.
 * 
 * Usage:
 * var graphController = new escape_escapeUi.GraphController(objectDetailsElement, aggregationId, aggregationExternalUri, selectionHashParameterName, graphViewerSelectHandler);
 * graphController.setGraphViewerSelectHandler(graphViewerSelectHandler);
 * graphController.startUrlPoller(100);
 * 
 * graphController.selectObjectByExternalUri('http://foo.bar/abc');
 * graphController.selectObjectByUri('escape:123');
 * graphController.selectObjectById('123');
 */
escape_escapeUi.GraphController = Class.create({
	instanceVariableName: null,
	objectDetailsElement: null,
	aggregationId: null,
	aggregationExternalUri: null,
	selectedObjectId: null,
	selectionHashParameterName: null,
	graphViewerSelectHandler: null,
	urlHashParameterPoller: null,
	
	initialize: function (instanceVariableName, objectDetailsElement, aggregationId, aggregationExternalUri, selectionHashParameterName)
	{
		this.instanceVariableName = instanceVariableName;
		this.objectDetailsElement = objectDetailsElement;
		this.aggregationId = aggregationId;
		this.aggregationExternalUri = aggregationExternalUri;
		this.selectionHashParameterName = selectionHashParameterName;
	},

	setGraphViewerSelectHandler: function (graphViewerSelectHandler)
	{
		this.graphViewerSelectHandler = graphViewerSelectHandler;
	},
	
	selectObjectByExternalUriWithoutViewerUpdate: function (objectExternalUri)
	{
		this.selectObjectByExternalUri(objectExternalUri, true);
	},
	
	selectObjectByExternalUri: function (objectExternalUri, noViewerUpdate)
	{
		if (objectExternalUri === this.aggregationExternalUri)
		{
			this.selectObjectById(this.aggregationId, noViewerUpdate);
		}
		else
		{
			escape_escapeUi.doRpcJsonRequest("GetAggregatedResourceIdByExternalUri", {aggregationId: this.aggregationId, externalUri: objectExternalUri}, this.selectObjectByIdWithoutViewerUpdate.bind(this), null);
		}
	},

	selectObjectByUri: function (objectUri, noViewerUpdate)
	{
		this.selectObjectById(escape_escapeUi.convertUriToId(objectUri), noViewerUpdate);
	},
	
	selectObjectByIdWithoutViewerUpdate: function (objectId)
	{
		this.selectObjectById(objectId, true);
	},

	selectObjectById: function (objectId, noViewerUpdate)
	{
		if (this.selectedObjectId !== objectId)
		{
			this.selectedObjectId = objectId;
			
			// update the URL hash parameter
			escape_escapeUi.setUrlHashParameter(this.selectionHashParameterName, objectId);
			
			//TODO: replace with loading indicator?
			//this.objectDetailsElement.insert({top: '<p><em>Loading data, please wait...</em></p>'});
			
			// update the page content
			new Ajax.Updater(this.objectDetailsElement, '/rpc', {
				method: 'post',
				evalScripts: true,
				parameters: { handler: 'GetObjectPageFragment', objectId: objectId, selectOnClickHandler: this.instanceVariableName + '.selectObjectById', displayGraph: true, gadget: escape_escapeUi.gadgetMode ? 1 : 0 }
			});

			if (this.graphViewerSelectHandler && noViewerUpdate !== true)
			{
				this.graphViewerSelectHandler(objectId);
				//escape_escapeUi.doRpcJsonRequest("GetOreObjectExternalUriById", {objectId: objectId}, this.graphViewerSelectHandler, null);
			}
		}
	},
	
	startUrlPoller: function (interval)
	{
		// display the currently selected object
		var selectedObjectId = escape_escapeUi.getUrlHashParameter(this.selectionHashParameterName);
		if (selectedObjectId === null)
		{
			// no object selected, display the aggregation instead
			selectedObjectId = this.aggregationId;
		}
		this.selectObjectById(selectedObjectId);
		
		// start polling the URL
		this.urlHashParameterPoller = new escape_escapeUi.UrlHashParameterPoller(this.selectionHashParameterName, interval, this.selectObjectById.bind(this));
	},
	
	stopUrlPoller: function ()
	{
		this.urlHashParameterPoller.stop();
	}
});

/**
 * Turns a textarea into an auto resizing textarea.
 * 
 * Usage:
 * new escape_escapeUi.ResizingTextArea(textareaElement)
 * 
 * To immediately resize the element:
 * (new escape_escapeUi.ResizingTextArea(textareaElement)).resize();
 */
escape_escapeUi.ResizingTextArea = Class.create({
	defaultRows: 1,
	maxRows: 20,
	textareaElement: null,

	initialize: function (textareaElement)
	{
		this.textareaElement = textareaElement;
		this.defaultRows = Math.max(textareaElement.rows, 1);

		textareaElement.observe('click', this.resizeNeeded.bind(this));
		textareaElement.observe('keyup', this.resizeNeeded.bind(this));
		
	},

	resizeNeeded: function (event)
	{
		this.resize();
	},
	
	resize: function ()
	{
		var textareaElement = this.textareaElement;
		
		var lines = textareaElement.value.split('\n');
		var newRows = lines.length + 1;
		var oldRows = textareaElement.rows;
		for (var i = 0; i < lines.length; i++)
		{
			var line = lines[i];
			if (line.length >= textareaElement.cols)
			{
				newRows += Math.floor(line.length / textareaElement.cols);
			}
		}
		
		newRows = Math.min(newRows, this.maxRows);
		
		if (newRows > textareaElement.rows)
		{
			textareaElement.rows = newRows;
		}
		if (newRows < textareaElement.rows)
		{
			textareaElement.rows = Math.max(this.defaultRows, newRows);
		}
	}
});

/**
 * Smooth scrolls to an element.
 * usage:
 * escape_escapeUi.Scroller.scrollTo(element)
 */
escape_escapeUi.Scroller = {
	speed: 15,
	interval: null,
	offsetTop: 0,
	scrollTo: function (element)
	{
		var elementTop = element.offsetTop;
		if (element.offsetParent)
		{
			while (element = element.offsetParent)
			{
				elementTop += element.offsetTop;
			}
		}
		
		if (escape_escapeUi.Scroller.interval)
		{
			clearInterval(escape_escapeUi.Scroller.interval);
		}
		escape_escapeUi.Scroller.interval = setInterval('escape_escapeUi.Scroller.scroll(' + elementTop + ')', 10);
	},
	scroll: function (targetTop)
	{
//		var windowInnerHeight = window.innerHeight || document.documentElement.clientHeight;
//		var bodyScrollHeight = document.body.scrollHeight;
		var scrollTop = escape_escapeUi.Scroller.scrollTop();
		
		if (targetTop > scrollTop)
		{
//			if (bodyScrollHeight - targetTop > windowInnerHeight)
//			{
				scrollTop += Math.ceil((targetTop - scrollTop) / escape_escapeUi.Scroller.speed);
//			}
//			else
//			{
//				scrollTop += Math.ceil((targetTop - scrollTop - (bodyScrollHeight - targetTop)) / escape_escapeUi.Scroller.speed);
//			}
		}
		else
		{
			scrollTop += (targetTop - scrollTop) / escape_escapeUi.Scroller.speed;
		}
		
		window.scrollTo(0, scrollTop);
		if (scrollTop === targetTop || escape_escapeUi.Scroller.offsetTop === scrollTop)
		{
			clearInterval(escape_escapeUi.Scroller.interval);
		}
		escape_escapeUi.Scroller.offsetTop = scrollTop;
	},
	scrollTop: function ()
	{
		var body = document.body;
		var documentElement = document.documentElement;
		if (body && body.scrollTop)
		{
			return body.scrollTop;
		}
		if (documentElement && documentElement.scrollTop)
		{
			return documentElement.scrollTop;
		}
		if (window.pageYOffset)
		{
			return window.pageYOffset;
		}
		return 0;
	}
};

/**
 * Create a HTML tooltip.
 * The first parameter is the element or element id to connect the tooltip to.
 * The second parameter is the content of the tooltip as an element or a HTML string.
 */
escape_escapeUi.HtmlTooltip = Class.create({
	initialize: function (element, content) {
		this.element = $(element);
		
		this.tooltipElement = new Element('div', {'class': 'escape_Tooltip'});
		this.tooltipElement.update(content);
		this.tooltipElement.hide();
		document.body.appendChild(this.tooltipElement);
		
		this.tooltip = new Tooltip(this.element, this.tooltipElement);
	}
});

escape_escapeUi.createInfoTooltip = function (content)
{
	var infoImg = new Element('img', {'alt': '', 'src': '/rsc/img/info.gif'});

	var infoSpan = new Element('span');
	infoSpan.insert(infoImg);
	
	var tooltip = new escape_escapeUi.HtmlTooltip(infoImg, content);
	
	return infoSpan;
};

/**
 * Perform a RPC request, expects a JSON result.
 * The onSuccess handler receives the parsed JSON data as the first parameter.
 * The (optional) onFailure handler receives the response object as the first parameter.
 * On failure or when unable to parse the JSON data, an alert message is shown containing the response text.
 */
escape_escapeUi.doRpcJsonRequest = function (handler, parameters, onSuccess, onFailure)
{
	parameters.handler = handler;
	new Ajax.Request('/rpc', {
		'method': 'post',
		'onSuccess':
			function (response)
			{
				if (response.responseJSON)
				{
					var json = response.responseJSON;
					onSuccess(json);
				}
				else
				{
					alert(response.responseText);
					if (onFailure)
					{
						onFailure(response);
					}
				}
			},
		'onFailure':
			function (response)
			{
				alert(response.responseText);
				if (onFailure)
				{
					onFailure(response);
				}
			},
		'parameters': parameters
	});
};

/**
 * Convert an object URI to an object ID.
 */
escape_escapeUi.convertUriToId = function (uri)
{
	return uri.replace(/info:fedora\/[^:]+:/, '', uri);
};

/**
 * Redirect to a resource identified by an ID.
 * Possible actions show, edit, resource, aggregation, rem
 */
escape_escapeUi.redirectToResourceById = function (action, id)
{
	var targetUrl = '/' + encodeURIComponent(action);
	if (typeof id != 'undefined')
	{
		targetUrl += '/' + encodeURIComponent(id);
	}
	if (escape_escapeUi.gadgetMode)
	{
		targetUrl += '?gadget=1';
	}
	location.href = targetUrl;
};

/**
 * Redirect to a resource identified by a URI.
 * Possible actions show, edit, resource, aggregation, rem
 */
escape_escapeUi.redirectToResourceByUri = function (action, uri)
{
	escape_escapeUi.redirectToResourceById(action, escape_escapeUi.convertUriToId(uri));
};


/**
 * UI component to create new aggregated resources.
 * usage:
 * new escape_escapeUi.OreAggregatedResourceCreator(containerElement, classUris)
 * 
 * It expects to be placed inside a form with an input element named "value" and a button named "save".
 * It fetches the class selector trees and displays them.
 * After a user selects a type, it will show a title / uri input field with the possibility to extract metadata from the resource.
 * Upon clicking the "add" button, it will create the new aggregated resource on the server, copy the new object URI to the "value" input element and triggers a "click" event on the "save" button.
 * The encapsulating form should the deal with value and click event.
 */
escape_escapeUi.OreAggregatedResourceCreator = Class.create({
	// constructor
	// containerElement is the element in which the UI component should be displayed
	// classUris is an array of class uris used to determine which (sub)classes should be displayed
	initialize: function (containerElement, classUris)
	{
		this.containerElement = containerElement;
		this.classUris = classUris;
		
		containerElement.update('Loading, please wait...');
		
		// fetch the class tree data from the server and display the class selector 
		escape_escapeUi.doRpcJsonRequest('GetClassSelectorTreesJson', {'classUris[]': this.classUris}, this.displayClassSelector.bind(this));
	},
	// display the class selector (for creating a new object)
	displayClassSelector: function (classSelectorTrees)
	{
		var containerElement = this.containerElement;

		containerElement.update();

		containerElement.insert(new Element('div').update('Select the type of the new object:'));

		var ul = new Element('ul', { 'class': 'escape_NestingNoFirstLevelMarker' });
		containerElement.appendChild(ul);

		for (var idx = 0, len = classSelectorTrees.length ; idx < len ; ++idx)
		{
			var classSelectorSubTree = classSelectorTrees[idx];

			var li = new Element('li');
			ul.appendChild(li);

			this.displayClassSelectorTreeRecursive(li, classSelectorSubTree, 0);
		}

		containerElement.insert(new Element('br'));
	},
	// recursive function to build the class selector trees
	displayClassSelectorTreeRecursive: function (parentElement, classSelectorSubTree, level)
	{
		// fetch the children
		var children = classSelectorSubTree.children;

		// display the label
		var labelSpan = new Element('span');
		parentElement.insert(labelSpan);

		var imgExpand = null;
		var imgCollapse = null;
		if (level === 0)
		{
			if (children.length > 0)
			{
				imgExpand = new Element('img', { 'src': '/rsc/img/expand.gif', 'style': 'vertical-align: top' });
				labelSpan.insert(imgExpand);
				imgCollapse = new Element('img', { 'src': '/rsc/img/collapse.gif', 'style': 'vertical-align: top' });
				imgCollapse.hide();
				labelSpan.insert(imgCollapse);
			}
			else
			{
				var imgNoExpand = new Element('img', { 'src': '/rsc/img/noexpand.gif', 'style': 'vertical-align: top' });
				labelSpan.insert(imgNoExpand);
			}
			labelSpan.insert('&nbsp;');
		}
		
		labelSpan.insert(new Element('span').update(classSelectorSubTree.label.escapeHTML()));

		labelSpan.insert('&nbsp;');

		// display the action links
		var actionsSpan = new Element('span', { 'class': 'escape_Actions' });
		parentElement.insert(actionsSpan);

		actionsSpan.insert('[ ');

		// display an action link to select the class 
		var selectLink = new Element('a', { 'href': '#' });
		selectLink.update('Select');
		actionsSpan.insert(selectLink);
		// when clicked, the metadata form is displayed 
		selectLink.observe('click', function (event) { event.stop(); this.displayMetadataForm(classSelectorSubTree.uri, classSelectorSubTree.label, classSelectorSubTree.titleUri, classSelectorSubTree.titleLabel); }.bind(this));

		// if a comment is available for the class, add it as an invisible div and an action link to toggle it's visibility
		if (classSelectorSubTree.comment)
		{
			actionsSpan.insert('&nbsp;| ');
			
			var commentDiv = new Element('div', { 'style': 'display: none; font-size: 85%; font-style: italic;' }).update(classSelectorSubTree.comment.escapeHTML());
			parentElement.insert(commentDiv);
			
			var aboutLink = new Element('a', { 'href': '#' });
			aboutLink.update('About');
			aboutLink.observe('click', function (event) { event.stop(); commentDiv.toggle(); });
			actionsSpan.insert(aboutLink);
		}
		
		actionsSpan.insert('&nbsp;]');
		
		if (children.length > 0)
		{
			// display the children
			var ul = new Element('ul');

			// hide the children of level 0
			if (level === 0)
			{
				ul.hide();
				labelSpan.observe('click', function () { ul.toggle(); imgExpand.toggle(); imgCollapse.toggle(); });
				labelSpan.title = 'Click to expand';
			}
			
			for (var idx = 0, len = children.length ; idx < len ; ++idx)
			{
				var child = children[idx];

				var li = new Element('li');
				ul.insert(li);

				// recurse to the grand children
				this.displayClassSelectorTreeRecursive(li, child, level + 1);
			}
			parentElement.insert(ul);
		}
	},
	// display the metadata form
	displayMetadataForm: function (typeUri, typeLabel, titleUri, titleLabel)
	{
		var containerElement = this.containerElement;
		
		// scroll to the input form
		//containerElement.scrollTo();
		escape_escapeUi.Scroller.scrollTo(containerElement);

		containerElement.update();

		// build the metadata input form
		var table = new Element('table', { 'class': 'escape_FormTable' });
		containerElement.insert(table);

		var tableBody = new Element('tbody');
		table.insert(tableBody);

		// heading row
		var titleHeadingTr = new Element('tr');
		tableBody.insert(titleHeadingTr);

		var titleHeadingTh = new Element('th', { 'colspan': 2 });
		titleHeadingTh.update('New ' + typeLabel.escapeHTML());
		titleHeadingTr.insert(titleHeadingTh);

		// add the type URI as a hidden input element
		var typeUriInput = new Element('input', { 'name': 'typeUri', 'type': 'hidden', 'value': typeUri });
		titleHeadingTh.insert(typeUriInput);

		// add the type label as a hidden input element
		var typeLabelInput = new Element('input', { 'name': 'typeLabel', 'type': 'hidden', 'value': typeLabel });
		titleHeadingTh.insert(typeLabelInput);
		
		// external uri row
		var externalUriTr = new Element('tr');
		tableBody.insert(externalUriTr);
		
		externalUriTr.insert(new Element('td').update('URL:'));
		
		var externalUriInputTd = new Element('td');
		externalUriTr.insert(externalUriInputTd);

		// input element for the URL
		var externalUriInput = new Element('input', { 'name': 'externalUri', 'type': 'text', 'size': 40 });
		externalUriInputTd.insert(externalUriInput);

		externalUriInputTd.insert('&nbsp;');

		// metadata extraction button
		var metadataExtractButton = new Element('button', { 'title': 'Extract metadata' });
		metadataExtractButton.update('Extract metadata');
		metadataExtractButton.update('<img src="/rsc/img/search.gif" alt="Extract metadata" />');
		externalUriInputTd.insert(metadataExtractButton);

		externalUriInputTd.insert('&nbsp;');

		var urlHelpText = 'The URL of an object is that which uniquely identifies the object.\n';
		urlHelpText += 'For a publication this might the URL of the full text.\n';
		urlHelpText += 'If available, "official" or "persistent" URLs are preferred.\n'; 
		urlHelpText += 'The URL field should be left empty for persons or organizations.\n'; 
		urlHelpText += 'Personal or organizational homepages can be indicated using the "Page" or "Personal homepage" properties.\n'; 
		urlHelpText += '<br /><br />\n'; 
		urlHelpText += 'You can automatically extract information (metadata) from the website of the object.\n';
		urlHelpText += 'To extract metadata, fill in the URL field above and click the <img src="/rsc/img/search.gif" alt="Extract metadata" title="Extract metadata" /> button.\n';
		urlHelpText += 'ESCAPE will attempt to extract metadata using several methods.\n';
		urlHelpText += 'The results will be displayed below.\n';
		urlHelpText += 'You can use the checkboxes to select which metadata you want to use for creating the object.\n';

		externalUriInputTd.insert(escape_escapeUi.createInfoTooltip(urlHelpText));

		// loading indicator
		var loadingSpan = new Element('span', { 'class': 'escape_LoadingIndicator', 'style': 'display: none' });
		
		loadingSpan.insert('&nbsp;');

		var loadingImg = new Element('img', { 'src': '/rsc/img/loading.gif', 'alt': 'loading' });
		loadingSpan.insert(loadingImg);
		
		externalUriInputTd.insert(loadingSpan);
		
		// title row
		var titleTr = new Element('tr');
		tableBody.insert(titleTr);

		titleTr.insert(new Element('td').update(titleLabel.escapeHTML() + ':'));
		
		var titleInputTd = new Element('td');
		titleTr.insert(titleInputTd);

		// input element for the title
		var titleInput = new Element('input', { 'name': 'title', 'type': 'text', 'size': 40 });
		titleInputTd.insert(titleInput);

		// add the title label as a hidden input element
		var titleLabelInput = new Element('input', { 'name': 'titleLabel', 'type': 'hidden', 'value': titleLabel });
		titleInputTd.insert(titleLabelInput);
		
		// add the title URI as a hidden input element
		var titleUriInput = new Element('input', { 'name': 'titleUri', 'type': 'hidden', 'value': titleUri });
		titleInputTd.insert(titleUriInput);
		
		// metadata row
		var metadataTr = new Element('tr');
		tableBody.insert(metadataTr);
		
		var metadataTd = new Element('td', { 'colspan': 2 });
		metadataTr.insert(metadataTd);

		var metadataDiv = new Element('div');
		metadataTd.insert(metadataDiv);
		
		// action row
		var actionTr = new Element('tr');
		tableBody.insert(actionTr);
		
		var actionTd = new Element('td', { 'colspan': 2 });
		actionTr.insert(actionTd);

		// button to create the resource
		var actionAddButton = new Element('button', { name: 'add_new' });
		actionAddButton.update('Add new ' + typeLabel.escapeHTML());
		actionTd.insert(actionAddButton);

		// add onclick behaviour to actionAddButton
		actionAddButton.observe('click', this.metadataFormAddButtonOnClick.bind(this));
		
		// add onclick behaviour to metadataExtractButton
		metadataExtractButton.observe('click',
				function (event)
				{
					event.stop();
					var element = event.element();

					if ($F(externalUriInput).strip() === "")
					{
						alert("Please fill in the URL field");
					}
					else
					{
						// extract metadata from the URL and display it in the form
						escape_escapeUi.doRpcJsonRequest('ExtractMetadata', {aggregationUri: oreObjectEditorAggregationUri, typeUri: typeUri, resourceUrl: $F(externalUriInput)}, this.displayExtractedMetadata.bind(this, metadataDiv));

						// show the loading indicator
						this.containerElement.select('.escape_LoadingIndicator').invoke('show');
						
						// disable the extract button
						element.disabled = true;
					}
				}.bind(this));

		// add onkeyup behaviour to externalUriInput
		externalUriInput.observe('keyup',
				function (event)
				{
					// (re)enable the extract button when the URL changes
					metadataExtractButton.disabled = false;
				}.bind(this));
		
		containerElement.insert(new Element('br'));
	},
	// display the extracted metadata
	displayExtractedMetadata: function (metadataElement, metadata)
	{
		var html = '';

		// hide the loading indicator
		this.containerElement.select('.escape_LoadingIndicator').invoke('hide');

		var idx = 0;
		for (var extractorIdx = 0 ; extractorIdx < metadata.length ; extractorIdx++)
		{
			var extractorResult = metadata[extractorIdx];
//			var extractorId = extractorResult['id'];
			var extractorName = extractorResult.name;
			var extractorMetadata = extractorResult.metadata;
			
			// show the title of the extractor and links to select all / none of it's rows
			html += '<p><strong>' + escape_escapeUi.escapeHTML(extractorName) + '</strong> (<a href="#" onclick="$$(\'.checkbox_' + extractorIdx + '\').each(function (checkbox){checkbox.checked = true;}); return false">Select all</a> / <a href="#" onclick="$$(\'.checkbox_' + extractorIdx + '\').each(function (checkbox){checkbox.checked = false;}); return false">none</a>)</p>';
			//html += '<blockquote>';
			html += '<table>';
			
			for (var rowIdx = 0; rowIdx < extractorMetadata.length ; rowIdx++)
			{
				var metadataRow = extractorMetadata[rowIdx];
				var label = metadataRow.label;
				var predicate = metadataRow.predicate;
				var object = metadataRow.object;
				var matchedRelatedObjects = metadataRow.matchedRelatedObjects;
				var classSelectorTrees = metadataRow.classSelectorTrees;
				var relatedObjectRdfXml = metadataRow.relatedObjectRdfXml;

				if (!relatedObjectRdfXml)
				{
					relatedObjectRdfXml = '';
				}

				var isRelation = false;
				var hasSingleZeroDifferenceMatch = false;
				var matchedRelatedObjectCount = 0;
				var classSelectorTreeCount = 0;
				if (classSelectorTrees)
				{
					isRelation = true;

					matchedRelatedObjectCount = matchedRelatedObjects.length;
					for (var matchedRelatedObjectIdx = 0 ; matchedRelatedObjectIdx < matchedRelatedObjectCount ; matchedRelatedObjectIdx++)
					{
						var matchedRelatedObject = matchedRelatedObjects[matchedRelatedObjectIdx];
						if (matchedRelatedObject.score == 0)
						{
							if (!hasSingleZeroDifferenceMatch)
							{
								hasSingleZeroDifferenceMatch = true;
							}
							else
							{
								hasSingleZeroDifferenceMatch = false;
								break;
							}
						}
					}
					
					classSelectorTreeCount = classSelectorTrees.length;
				}
				
				html += '<tr class="meta_row">';
				
				html += '<td>';
				html += '<input name="meta_selected" class="checkbox_' + extractorIdx + '" type="checkbox" />';
				html += '</td>';
				
				html += '<td>';
				html += '<input name="meta_predicate" type="hidden" value="' + escape_escapeUi.escapeHTML(predicate) + '" />';
				html += '<strong title="' + escape_escapeUi.escapeHTML(predicate) + '">';
				html += escape_escapeUi.escapeHTML(label) + ':&nbsp;';
				html += '</strong>';
				html += '</td>';

				html += '<td>';
				html += '<input name="meta_relatedObjectRdfXml" type="hidden" value="' + escape_escapeUi.escapeHTML(relatedObjectRdfXml) + '" />';
				if (!isRelation)
				{
					html += '<input name="meta_object" type="hidden" value="' + escape_escapeUi.escapeHTML(object) + '" />';
					html += '<div>';
					html += escape_escapeUi.escapeHTML(object);
					html += '<div>';
				}
				else
				{
					html += '<div>';
					html += '<em>';
					html += escape_escapeUi.escapeHTML(object);
					html += '</em>';
					html += '<div>';
					
					html += '&#8594;&nbsp;<select name="meta_object" style="width: 250px">';
					if (matchedRelatedObjectCount > 0)
					{
						html += '<option value="">';
						html += '----- Matched objects -----';
						html += '</option>';
						for (var matchedRelatedObjectIdx = 0 ; matchedRelatedObjectIdx < matchedRelatedObjectCount ; matchedRelatedObjectIdx++)
						{
							var matchedRelatedObject = matchedRelatedObjects[matchedRelatedObjectIdx];
							
							var selected = '';
							if (matchedRelatedObject.score == 0 && hasSingleZeroDifferenceMatch)
							{
								selected = ' selected="selected"';
							}
							
							html += '<option value="' + escape_escapeUi.escapeHTML(matchedRelatedObject.uri) + '"' + selected + '>';
							html += '&nbsp;&nbsp;&nbsp;';
							html += escape_escapeUi.escapeHTML(matchedRelatedObject.title) + ' (' + escape_escapeUi.escapeHTML(matchedRelatedObject.typeLabel) + ')';
//							html += ' ' + matchedRelatedObject.score;
							html += '</option>';
						}
					}
					if (classSelectorTreeCount > 0)
					{
						html += '<option value="">';
						html += '----- Create a new object -----';
						html += '</option>';
						
						var getClassSelectorOptionsRecursive = function (classSelectorTree, level)
						{
							var html = '';
							html += '<option value="[[new::' + escape_escapeUi.escapeHTML(classSelectorTree.uri) + ']]">';
							for (var levelIdx = 0 ; levelIdx <= level ; levelIdx++)
							{
								html += '&nbsp;&nbsp;&nbsp;';
							}
							html += classSelectorTree.label;
							html += '</option>';
							
							level++;
							
							var children = classSelectorTree.children;
							if (children)
							{
								var childCount = children.length;
								for (var childIdx = 0 ; childIdx < childCount ; childIdx++)
								{
									var child = children[childIdx];
									html += getClassSelectorOptionsRecursive(child, level);
								}
							}
							
							return html;
						};
						
						for (var classSelectorTreeIdx = 0 ; classSelectorTreeIdx < classSelectorTreeCount ; classSelectorTreeIdx++)
						{
							var classSelectorTree = classSelectorTrees[classSelectorTreeIdx];
							
							html += getClassSelectorOptionsRecursive(classSelectorTree, 0);
						}
					}
					html += '</select>';
				}
				html += '</td>';
				
				html += '</tr>';
				
				idx++;
			}
			
			html += '</table>';
			//html += '</blockquote>';
		}

		metadataElement.update(html);
	},
	// get the selected metadata rows
	getSelectedExtractedMetadata: function (form)
	{
		var result = [];
		
		var metaRows = form.select('.meta_row');
		
		for (var metaRowIdx = 0, metaRowCount = metaRows.length ; metaRowIdx < metaRowCount ; metaRowIdx++)
		{
			var metaRow = metaRows[metaRowIdx];
			
			var selected = metaRow.select('input[name="meta_selected"]')[0].checked;
			if (selected)
			{
				var predicate = metaRow.select('input[name="meta_predicate"]')[0].value;
				var object = metaRow.select('input[name="meta_object"],select[name="meta_object"]')[0].value;
				var relatedObjectRdfXml = metaRow.select('input[name="meta_relatedObjectRdfXml"]')[0].value;
				
				var resultRow = new Hash();
				resultRow.set('predicate', predicate);
				resultRow.set('object', object);
				if (relatedObjectRdfXml)
				{
					resultRow.set('relatedObjectRdfXml', relatedObjectRdfXml);
				}
				
				result.push(resultRow);
			}
		}
		
		return result;
	},
	// handle add button click events from the metadata form
	metadataFormAddButtonOnClick: function (event)
	{
		event.stop();
		
		var button = event.element();

		// fetch data
		var form = $(button.form);
		var titleLabel = form.select('input[name="titleLabel"]')[0].value;
		var title = form.select('input[name="title"]')[0].value;
		var titleUri = form.select('input[name="titleUri"]')[0].value;
		var typeUri = form.select('input[name="typeUri"]')[0].value;
//		var typeLabel = form.select('input[name="typeLabel"]')[0].value;
		var externalUri = form.select('input[name="externalUri"]')[0].value;
		
		// check if a title was entered or selected from the extracted metadata
		var hasTitle = false;
		var metadata = this.getSelectedExtractedMetadata(form);
		for (var metadataIdx = 0, metadataLength = metadata.length ; metadataIdx < metadataLength ; metadataIdx++)
		{
			var metadataRow = metadata[metadataIdx];
			if (metadataRow.get('predicate') === titleUri)
			{
				hasTitle = true;
				break;
			}
		}
		
		// check if there are missing relations
		var hasMissingRelations = false;
		for (var metadataIdx = 0, metadataLength = metadata.length ; metadataIdx < metadataLength ; metadataIdx++)
		{
			var metadataRow = metadata[metadataIdx];
			if (metadataRow.get('object') === '')
			{
				hasMissingRelations = true;
				break;
			}
		}

		if (!hasTitle && title.strip() === '')
		{
			// give a warning if no title was entered
			alert("Please fill in the " + titleLabel + " field.");
		}
		else if (hasMissingRelations)
		{
			// give a warning if there are missing relations
			alert("Please select an existing object or object type from the drop-down lists for every selected relation.");
		}
		else
		{
			// create the new resource
			var onSuccess = function (json) { form.select('[name="value"]')[0].value = json; form.select('[name="save"]')[0].click(); };
			var onFailure = function (response) { button.disabled = false; };
			escape_escapeUi.doRpcJsonRequest('CreateOreAggregatedResource', {aggregationUri: oreObjectEditorAggregationUri, typeUri: typeUri, externalUri: externalUri, titleUri: titleUri, titleValue: title, metadata: Object.toJSON(metadata)}, onSuccess, onFailure);
			
			// disable the add button while processing the request
			button.disabled = true;
		}
	}
});

function findUsers(containerElementId, query, onClickJsHandler)
{
	new Ajax.Updater(containerElementId, '/rpc', {
		method: 'post',
		evalScripts: true,
		parameters: { handler: 'FindUsers', query: query, onClickJsHandler: onClickJsHandler, gadget: escape_escapeUi.gadgetMode ? 1 : 0 }
	});
}

// add an owner to an aggregation and update the list editor (RPC)
function addOreAggregationOwner(containerElementId, aggregationUri, ownerUri)
{
	new Ajax.Updater(containerElementId, '/rpc', {
		method: 'post',
		evalScripts: true,
		parameters: { handler: 'AddOreAggregationOwner', containerElementId: containerElementId, aggregationUri: aggregationUri, ownerUri: ownerUri, gadget: escape_escapeUi.gadgetMode ? 1 : 0 }
	});
}

// remove an owner from an aggregation and update the list editor (RPC)
function removeOreAggregationOwner(containerElementId, aggregationUri, ownerUri)
{
	new Ajax.Updater(containerElementId, '/rpc', {
		method: 'post',
		evalScripts: true,
		parameters: { handler: 'RemoveOreAggregationOwner', containerElementId: containerElementId, aggregationUri: aggregationUri, ownerUri: ownerUri, gadget: escape_escapeUi.gadgetMode ? 1 : 0 }
	});
}

// add an editor to an aggregation and update the list editor (RPC)
function addOreAggregationEditor(containerElementId, aggregationUri, editorUri)
{
	new Ajax.Updater(containerElementId, '/rpc', {
		method: 'post',
		evalScripts: true,
		parameters: { handler: 'AddOreAggregationEditor', containerElementId: containerElementId, aggregationUri: aggregationUri, editorUri: editorUri, gadget: escape_escapeUi.gadgetMode ? 1 : 0 }
	});
}

// remove an editor from an aggregation and update the list editor (RPC)
function removeOreAggregationEditor(containerElementId, aggregationUri, editorUri)
{
	new Ajax.Updater(containerElementId, '/rpc', {
		method: 'post',
		evalScripts: true,
		parameters: { handler: 'RemoveOreAggregationEditor', containerElementId: containerElementId, aggregationUri: aggregationUri, editorUri: editorUri, gadget: escape_escapeUi.gadgetMode ? 1 : 0 }
	});
}

// get an ORE object editor property fragment (RPC)
function getOreObjectEditorPropertyFragment(containerElementId, subject, predicate, mode)
{
	new Ajax.Updater(containerElementId, '/rpc', {
		method: 'post',
		evalScripts: true,
		parameters: { handler: 'GetOreObjectEditorPropertyFragment', containerElementId: containerElementId, subject: subject, predicate: predicate, mode: mode, gadget: escape_escapeUi.gadgetMode ? 1 : 0 }
	});
}

// get an ORE object editor property value fragment (RPC)
function getOreObjectEditorPropertyValueFragment(containerElementId, subject, predicate, object, mode, single)
{
	new Ajax.Updater(containerElementId, '/rpc', {
		method: 'post',
		evalScripts: true,
		parameters: { handler: 'GetOreObjectEditorPropertyValueFragment', containerElementId: containerElementId, subject: subject, predicate: predicate, object: object, mode: mode, single: single, gadget: escape_escapeUi.gadgetMode ? 1 : 0 }
	});
}

//add a new value to the ORE object editor
function addOreObjectEditorPropertyValueFragment(containerElementId, subject, predicate)
{
	var newContainer = new Element('div');
	Element.insert(containerElementId, {before: newContainer});

	getOreObjectEditorPropertyValueFragment(newContainer.identify(), subject, predicate, '', 'new', false);
}	

// update an ORE object property value (RPC)
function updateOreObjectPropertyValue(containerElementId, subject, predicate, object, newObject, single)
{
	new Ajax.Updater(containerElementId, '/rpc', {
		method: 'post',
		evalScripts: true,
		parameters: { handler: 'UpdateOreObjectPropertyValue', containerElementId: containerElementId, subject: subject, predicate: predicate, object: object, newObject: newObject, single: single, gadget: escape_escapeUi.gadgetMode ? 1 : 0 }
	});
}

// add an ORE object property value (RPC)
function addOreObjectPropertyValue(containerElementId, subject, predicate, object)
{
	new Ajax.Updater(containerElementId, '/rpc', {
		method: 'post',
		evalScripts: true,
		parameters: { handler: 'AddOreObjectPropertyValue', containerElementId: containerElementId, subject: subject, predicate: predicate, object: object, gadget: escape_escapeUi.gadgetMode ? 1 : 0 }
	});
}

// remove an ORE object property value (RPC)
function removeOreObjectPropertyValue(subject, predicate, object)
{
	new Ajax.Request('/rpc', {
		method: 'post',
		evalScripts: true,
		parameters: { handler: 'RemoveOreObjectPropertyValue', subject: subject, predicate: predicate, object: object, onFailure: function (transport) { alert(transport.responseText); }, gadget: escape_escapeUi.gadgetMode ? 1 : 0 }
	});
}

//add a new predicate to the ORE object editor or select an existing one
function addOreObjectEditorPredicate(subject, predicate, mode)
{
	if (predicateTableRowElementIdMappings[predicate])
	{
		var tableRow = $(predicateTableRowElementIdMappings[predicate]);
		var containers = tableRow.childElements()[1].childElements();
		var lastContainer = containers[containers.length - 1];
		addOreObjectEditorPropertyValueFragment(lastContainer.identify(), subject, predicate);

		//tableRow.scrollTo();
		escape_escapeUi.Scroller.scrollTo(tableRow);
	}
	else
	{
		var tableElement = $('oreObjectEditorPropertiesTable');

		var tbodyElement = tableElement.select('tbody')[0];

		var tableRow = new Element('tr');
		tbodyElement.appendChild(tableRow);

		var tableRowId = tableRow.identify();
		predicateTableRowElementIdMappings[predicate] = tableRowId;
		
		getOreObjectEditorPropertyFragment(tableRowId, subject, predicate, mode);

		//tableRow.scrollTo();
		escape_escapeUi.Scroller.scrollTo(tableRow);
	}
}

/**
 * Invoked by escape_escapeUi.startSessionStatusPoller (notLoggedInWarningElement, pollingInterval).
 */
escape_escapeUi.checkSessionStatus = function (notLoggedInWarningElement, pollingInterval) {
	notLoggedInWarningElement = $(notLoggedInWarningElement);

	var getSessionStatusOnSuccess = function (sessionStatus) {
		if (!sessionStatus.userId) {
			notLoggedInWarningElement.show();
		}
		else {
			notLoggedInWarningElement.hide();
		}
		escape_escapeUi.startSessionStatusPoller(notLoggedInWarningElement, pollingInterval);
	};

	var getSessionStatusOnFailure = function () {
		notLoggedInWarningElement.show();
		escape_escapeUi.startSessionStatusPoller(notLoggedInWarningElement, pollingInterval);
	};

	escape_escapeUi.doRpcJsonRequest('GetSessionStatus', {}, getSessionStatusOnSuccess, getSessionStatusOnFailure);
};

/**
 * Start a session status poller.
 * If the user is no longer logged in, notLoggedInWarningElement (a div with style="display: none") will be set to visible.
 * After the user logs on, notLoggedInWarningElement will be made invisible.
 * 
 * pollingInterval specifies the polling interval in milliseconds.
 */
escape_escapeUi.startSessionStatusPoller = function (notLoggedInWarningElement, pollingInterval) {
	setTimeout(function () { escape_escapeUi.checkSessionStatus(notLoggedInWarningElement, pollingInterval); }, pollingInterval);
};

